/*
 * [The "BSD licence"]
 * Copyright (c) 2010 Ben Gruver (JesusFreke)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.jf.baksmali.Adaptors;

import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.jf.baksmali.BaksmaliOptions;
import org.jf.dexlib2.AccessFlags;
import org.jf.dexlib2.dexbacked.DexBackedClassDef;
import org.jf.dexlib2.dexbacked.DexBackedDexFile.InvalidItemIndex;
import org.jf.dexlib2.iface.Annotation;
import org.jf.dexlib2.iface.ClassDef;
import org.jf.dexlib2.iface.Field;
import org.jf.dexlib2.iface.Method;
import org.jf.dexlib2.iface.MethodImplementation;
import org.jf.dexlib2.iface.instruction.Instruction;
import org.jf.dexlib2.iface.instruction.formats.Instruction21c;
import org.jf.dexlib2.iface.reference.FieldReference;
import org.jf.dexlib2.util.ReferenceUtil;
import org.jf.util.IndentingWriter;
import org.jf.util.StringUtils;



public class ClassDefinition {
	
	public final BaksmaliOptions options;
	
	public final ClassDef classDef;
	
	private final HashSet<String> fieldsSetInStaticConstructor;

	protected boolean validationErrors;

	public static boolean source = false;

	public ClassDefinition(BaksmaliOptions options, ClassDef classDef) {
		this.options = options;
		this.classDef = classDef;
		fieldsSetInStaticConstructor = findFieldsSetInStaticConstructor(classDef);
	}

	public boolean hadValidationErrors() {
		return validationErrors;
	}

	
	private static HashSet<String> findFieldsSetInStaticConstructor(ClassDef classDef) {
		HashSet<String> fieldsSetInStaticConstructor = new HashSet<String>();

		for (Method method : classDef.getDirectMethods()) {
			if (method.getName().equals("<clinit>")) {
				MethodImplementation impl = method.getImplementation();
				if (impl != null) {
					for (Instruction instruction : impl.getInstructions()) {
						switch (instruction.getOpcode()) {
						case SPUT:
						case SPUT_BOOLEAN:
						case SPUT_BYTE:
						case SPUT_CHAR:
						case SPUT_OBJECT:
						case SPUT_SHORT:
						case SPUT_WIDE: {
							Instruction21c ins = (Instruction21c) instruction;
							FieldReference fieldRef = null;
							try {
								fieldRef = (FieldReference) ins.getReference();
							} catch (InvalidItemIndex ex) {
								// just ignore it for now. We'll deal with it
								// later, when processing the instructions
								// themselves
							}
							if (fieldRef != null && fieldRef.getDefiningClass().equals((classDef.getType()))) {
								fieldsSetInStaticConstructor.add(ReferenceUtil.getShortFieldDescriptor(fieldRef));
							}
							break;
						}
						}
					}
				}
			}
		}
		return fieldsSetInStaticConstructor;
	}

	public void writeTo(IndentingWriter writer) throws IOException {
		writeClass(writer);
		writeSuper(writer);
		writeSourceFile(writer);
		writeInterfaces(writer);
		writeAnnotations(writer);
		Set<String> staticFields = writeStaticFields(writer);
		writeInstanceFields(writer, staticFields);
		Set<String> directMethods = writeDirectMethods(writer);
		writeVirtualMethods(writer, directMethods);
	}

	private void writeClass(IndentingWriter writer) throws IOException {
		writer.write(".class ");
		writeAccessFlags(writer);
		writer.write(classDef.getType());
		writer.write('\n');
	}

	private void writeAccessFlags(IndentingWriter writer) throws IOException {
		for (AccessFlags accessFlag : AccessFlags.getAccessFlagsForClass(classDef.getAccessFlags())) {
			writer.write(accessFlag.toString());
			writer.write(' ');
		}
	}

	private void writeSuper(IndentingWriter writer) throws IOException {
		String superClass = classDef.getSuperclass();
		if (superClass != null) {
			writer.write(".super ");
			writer.write(superClass);
			writer.write('\n');
		}
	}

	private void writeSourceFile(IndentingWriter writer) throws IOException {
		if (source) {
			String sourceFile = classDef.getSourceFile();
			if (sourceFile != null) {
				writer.write(".source \"");
				StringUtils.writeEscapedString(writer, sourceFile);
				writer.write("\"\n");
			}
		}
	}

	private void writeInterfaces(IndentingWriter writer) throws IOException {
		List<String> interfaces = classDef.getInterfaces();

		if (interfaces.size() != 0) {
			writer.write('\n');
			writer.write("# interfaces\n");
			for (String interfaceName : interfaces) {
				writer.write(".implements ");
				writer.write(interfaceName);
				writer.write('\n');
			}
		}
	}

	private void writeAnnotations(IndentingWriter writer) throws IOException {
		Collection<? extends Annotation> classAnnotations = classDef.getAnnotations();
		if (classAnnotations.size() != 0) {
			writer.write("\n\n");
			writer.write("# annotations\n");

			String containingClass = null;
			if (options.implicitReferences) {
				containingClass = classDef.getType();
			}

			AnnotationFormatter.writeTo(writer, classAnnotations, containingClass);
		}
	}

	private Set<String> writeStaticFields(IndentingWriter writer) throws IOException {
		boolean wroteHeader = false;
		Set<String> writtenFields = new HashSet<String>();

		Iterable<? extends Field> staticFields;
		if (classDef instanceof DexBackedClassDef) {
			staticFields = ((DexBackedClassDef) classDef).getStaticFields(false);
		} else {
			staticFields = classDef.getStaticFields();
		}

		for (Field field : staticFields) {
			if (!wroteHeader) {
				writer.write("\n\n");
				writer.write("# static fields");
				wroteHeader = true;
			}
			writer.write('\n');

			boolean setInStaticConstructor;
			IndentingWriter fieldWriter = writer;
			String fieldString = ReferenceUtil.getShortFieldDescriptor(field);
			if (!writtenFields.add(fieldString)) {
				writer.write("# duplicate field ignored\n");
				fieldWriter = new CommentingIndentingWriter(writer);
				System.err.println(String.format("Ignoring duplicate field: %s->%s", classDef.getType(), fieldString));
				setInStaticConstructor = false;
			} else {
				setInStaticConstructor = fieldsSetInStaticConstructor.contains(fieldString);
			}
			FieldDefinition.writeTo(options, fieldWriter, field, setInStaticConstructor);
		}
		return writtenFields;
	}

	private void writeInstanceFields(IndentingWriter writer, Set<String> staticFields) throws IOException {
		boolean wroteHeader = false;
		Set<String> writtenFields = new HashSet<String>();

		Iterable<? extends Field> instanceFields;
		if (classDef instanceof DexBackedClassDef) {
			instanceFields = ((DexBackedClassDef) classDef).getInstanceFields(false);
		} else {
			instanceFields = classDef.getInstanceFields();
		}

		for (Field field : instanceFields) {
			if (!wroteHeader) {
				writer.write("\n\n");
				writer.write("# instance fields");
				wroteHeader = true;
			}
			writer.write('\n');

			IndentingWriter fieldWriter = writer;
			String fieldString = ReferenceUtil.getShortFieldDescriptor(field);
			if (!writtenFields.add(fieldString)) {
				writer.write("# duplicate field ignored\n");
				fieldWriter = new CommentingIndentingWriter(writer);
				System.err.println(String.format("Ignoring duplicate field: %s->%s", classDef.getType(), fieldString));
			} else if (staticFields.contains(fieldString)) {
				System.err.println(String.format("Duplicate static+instance field found: %s->%s", classDef.getType(),
						fieldString));
				System.err.println("You will need to rename one of these fields, including all references.");

				writer.write("# There is both a static and instance field with this signature.\n"
						+ "# You will need to rename one of these fields, including all references.\n");
			}
			FieldDefinition.writeTo(options, fieldWriter, field, false);
		}
	}

	private Set<String> writeDirectMethods(IndentingWriter writer) throws IOException {
		boolean wroteHeader = false;
		Set<String> writtenMethods = new HashSet<String>();

		Iterable<? extends Method> directMethods;
		if (classDef instanceof DexBackedClassDef) {
			directMethods = ((DexBackedClassDef) classDef).getDirectMethods(false);
		} else {
			directMethods = classDef.getDirectMethods();
		}

		for (Method method : directMethods) {
			if (!wroteHeader) {
				writer.write("\n\n");
				writer.write("# direct methods");
				wroteHeader = true;
			}
			writer.write('\n');

			// TODO: check for method validation errors
			String methodString = ReferenceUtil.getMethodDescriptor(method, true);

			IndentingWriter methodWriter = writer;
			if (!writtenMethods.add(methodString)) {
				writer.write("# duplicate method ignored\n");
				methodWriter = new CommentingIndentingWriter(writer);
			}

			MethodImplementation methodImpl = method.getImplementation();
			if (methodImpl == null) {
				MethodDefinition.writeEmptyMethodTo(methodWriter, method, options);
			} else {
				MethodDefinition methodDefinition = new MethodDefinition(this, method, methodImpl);
				methodDefinition.writeTo(methodWriter);
			}
		}
		return writtenMethods;
	}

	private void writeVirtualMethods(IndentingWriter writer, Set<String> directMethods) throws IOException {
		boolean wroteHeader = false;
		Set<String> writtenMethods = new HashSet<String>();

		Iterable<? extends Method> virtualMethods;
		if (classDef instanceof DexBackedClassDef) {
			virtualMethods = ((DexBackedClassDef) classDef).getVirtualMethods(false);
		} else {
			virtualMethods = classDef.getVirtualMethods();
		}

		for (Method method : virtualMethods) {
			if (!wroteHeader) {
				writer.write("\n\n");
				writer.write("# virtual methods");
				wroteHeader = true;
			}
			writer.write('\n');

			// TODO: check for method validation errors
			String methodString = ReferenceUtil.getMethodDescriptor(method, true);

			IndentingWriter methodWriter = writer;
			if (!writtenMethods.add(methodString)) {
				writer.write("# duplicate method ignored\n");
				methodWriter = new CommentingIndentingWriter(writer);
			} else if (directMethods.contains(methodString)) {
				writer.write("# There is both a direct and virtual method with this signature.\n"
						+ "# You will need to rename one of these methods, including all references.\n");
				System.err.println(String.format("Duplicate direct+virtual method found: %s->%s", classDef.getType(),
						methodString));
				System.err.println("You will need to rename one of these methods, including all references.");
			}

			MethodImplementation methodImpl = method.getImplementation();
			if (methodImpl == null) {
				MethodDefinition.writeEmptyMethodTo(methodWriter, method, options);
			} else {
				MethodDefinition methodDefinition = new MethodDefinition(this, method, methodImpl);
				methodDefinition.writeTo(methodWriter);
			}
		}
	}
}
